AWS CDK(Cloud Development Kit )を使用した各種のLambda設置
1 はじめに
CX事業本部の平内(SIN)です。
AWS Cloud Development Kit (以下、AWS CDK)では、TypeScriptを使用して、CFnのテンプレートが作成可能ですが、LambdaもTypeScriptで作成している場合、コードとリソースが一元管理できて、いい感じにまとまる気がして好きです。
今回は、Lambda関数を設置する場合の色々な場面について、AWS CDKの利用方法を確認してみました。
2 簡単なLambdaの設置
最も簡単なLambdaの設置例です。
lambda.Function()の第3パラメータであるFunctionPropsは、下記の3項目の設定が必須です。
- runtime
- code
- handler
functionName(関数名)は、必須ではないですが、認識しやすいように設定しました。
import cdk = require('@aws-cdk/core'); import * as lambda from '@aws-cdk/aws-lambda'; export class CdkLamdaSampleStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const myFunction = new lambda.Function( this, 'my-function', { functionName: 'my-function', runtime: lambda.Runtime.NODEJS_10_X, handler: 'index.handler', code: lambda.Code.asset('lambda') }) } }
また、codeの指定は、lambda.Code.asset()を使用し、階層下にディレクトリ(lambda)を作成して本体を設置しています。
$ tree -L 1 . ├── README.md ├── bin ├── cdk.json ├── cdk.out ├── lambda //このディレクトリを作成 ├── lib ├── node_modules ├── package-lock.json ├── package.json ├── tsconfig.json └── yarn.lock
AWS CDKのプロジェクトの階層下でTypeScriptのコードを展開する場合、トップのtsconfig.jsonに従って、トランスパイルされるため、同じフォルダ内にjsファイルが生成されることになります。
lambda/index.ts(簡単なLambda関数の例)
export async function handler(event:any) { console.log(JSON.stringify(event)) return { statusCode: 200, body: "hello" } }
3 デフォルトのポリシー
先のコードでは、Lambdaのロールに関して特に記述がありませんが、この場合、デフォルトのロールが自動的に生成されます。
生成されたロールを確認すると、管理ポリシー(AWSLambdaBasicExecutionRole)のみが設定されていることを確認できます。
4 ポリシーの追加
Lambda関数の中で、DynamoDBにアクセスするような場合、パーミッションの追加が必要になります。
lambda/index.ts (DynamoDBへのアクセス例)
import * as AWS from 'aws-sdk'; const client = new AWS.DynamoDB.DocumentClient(); const tableName = process.env.TABLE_NAME!; export async function handler(event:any) { console.log(JSON.stringify(event)); const id = '001'; const data = await client.get({ TableName: tableName, Key:{ 'id': id } }).promise(); return { statusCode: 200, body: data.Item!.message } }
AWS CDKでは、addToRolePolicyで簡単にパーミッションを追加できます。
import * as iam from '@aws-cdk/aws-iam'; //・・・略・・・ const tableArn = 'arn:aws:dynamodb:' + this.region + ':' + this.account + ':table/' + tableName; myFunction.addToRolePolicy(new iam.PolicyStatement({ resources: [tableArn], actions: ['dynamodb:GetItem'] } ));
追加後、Lambdaのロールは、以下のようになります。
上の例では、ポリシーのリソースを指定するために、TableのARNを文字列の連結で作成していますが、Table自体が、AWS CDKで作成されている場合、tableArnプロパティでARNが取得できます。
AWS CDKで生成しているリソースの場合、循環依存の回避ぐらいしか、ARN等を文字列操作で作成する必要は無いでしょう。
import * as dynamodb from '@aws-cdk/aws-dynamodb'; //・・・略・・・ // テーブルの作成 const table = new dynamodb.Table(this, 'sample-table', { tableName: tableName, partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING } }); myFunction.addToRolePolicy(new iam.PolicyStatement({ resources: [table.tableArn], // リソースのARNは、プロパティで指定 actions: ['dynamodb:GetItem'] } ));
ちなみに、DynamoDBテーブルには、各種のgrantメソッドが用意されており、こちらを使用してもLambdaにパーミッションを追加することができます。
下記では、テーブルにmyFunctionからのReadWriteを追加しています。
table.grantReadWriteData(myFunction);
このように、関連するリソースからLambdaを指定した場合も、必要なパーミッションが追加される仕組みが、AWS CDKにはあります。
5 環境変数
AWS CDK内で指定したリソースの名前などは、環境変数を使用してLambdaと同期させるのが適切でしょう。
下記では、テーブル名をAWS CDK側で定義し(const tableName = 'sample-table')、Lambdaへは、環境変数で渡しています。
const tableName = 'sample-table'; const myFunction = new lambda.Function( this, 'my-function', { functionName: 'my-function', runtime: lambda.Runtime.NODEJS_10_X, handler: 'index.handler', code: lambda.Code.asset('lambda'), environment:{ "TABLE_NAME": tableName } })
Lambda側では、環境変数から値を受け取って作業します。
const tableName = process.env.TABLE_NAME!;
6 API Gateway
Lambda関数をRestAPIのバックエンドに使用するパターンは、よくある話だと思います。そして この場合、AWS Gatewayを設置して、そこからLambdaを呼び出す仕組みを作るのですが、AWS CDKでは、次のような簡単なコードで、これが作成できます。
最初に、apigateway.RestApi()で、RestAPIのオブジェクトを生成し、次に、apigateway.LambdaIntegration()でLambdaを使用した接続先を生成します。最後に、RestAPIにメソッドとしてこれを追加する流れです。
import * as apigateway from '@aws-cdk/aws-apigateway'; //・・・略・・・ const api = new apigateway.RestApi(this, "api", { restApiName: 'lambda-sample' }); const integration = new apigateway.LambdaIntegration(myFunction); api.root.addMethod('POST', integration);
上のコードをdeployすると最後に下記のようにEndpointが表示されます。
✅ CdkLamdaSampleStack Outputs: CdkLamdaSampleStack.apiEndpoint9349E63C = https://yc9d0apm30.execute-api.ap-northeast-1.amazonaws.com/prod/
作成されたリソースを確認すると、次のようになっています。
そして、prodステージでデプロイも完了しています。
Endpointに対してリクエストするとLambdaがレスポンスできている事を確認できます。
$ curl -X POST https://yc9d0apm30.execute-api.ap-northeast-1.amazonaws.com/prod/ good morning
7 トリガー
Lambdaを他のサービスから使用するためのトリガーは、利用側のリソースのとの関連を記述した時点で、自動的に付与されます。
先の例では、API Gatewayの接続先として指定した時点で、これが完了していることになります。
const integration = new apigateway.LambdaIntegration(myFunction);
もう一つの例として、S3バケットにファイルが追加された時にLambdaが発火する仕組みを作ってみます。
手順としては、addEventNotification()でイベントの通知先をLambdaに指定する感じです。
import * as s3n from '@aws-cdk/aws-s3-notifications'; //・・・略・・・ const myBucket = new s3.Bucket(this, "my-bucket", { bucketName: "lambda-sample-bucket" }) myBucket.addEventNotification ( s3.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(myFunction));
8 ログの保持期間
Lambdaのログは、CloudWatchLogsに保存されますが、その 保存期間は、デフォルトで無期限となっています。
有効期限を設定したい場合は、ロググループ自体をAWS CDKで生成する必要があります。(注:Lambdaの実行などで、既にロググループが作成されている場合、deployに失敗します。ロググループが無い状態でスタックを作成して下さい)
import * as logs from '@aws-cdk/aws-logs'; //・・・略・・・ const loggroup = new logs.LogGroup(this, 'log-group', { logGroupName: '/aws/lambda/' + myFunction.functionName, retention: logs.RetentionDays.ONE_DAY })
9 CloudWatchのログ監視
エラーが発生した場合に、それを検出して通知などを行う処理は、よくあるパターンだと思います。 下記の例は、my-functionというLambdaのログに「Error」というパターンが見つかった際に、error-functionというLambdaを起動するようにしたものです。
my-functionの出力先であるロググループに、addSubscriptionFilter()でサブスクリプションを設定しています。そして、サブスクリプションの先は、error-functionに繋がっています。
import cdk = require('@aws-cdk/core'); import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as logsn from '@aws-cdk/aws-logs-destinations'; export class CdkLamdaSampleStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const myFunction = new lambda.Function( this, 'my-function', { functionName: 'my-function', runtime: lambda.Runtime.NODEJS_10_X, handler: 'index.handler', code: lambda.Code.asset('lambda'), }) const errorFunction = new lambda.Function( this, 'error-function', { functionName: 'error-function', runtime: lambda.Runtime.NODEJS_10_X, handler: 'index.handler', code: lambda.Code.asset('lambda/error'), }) const loggroup = new logs.LogGroup(this, 'log-group', { logGroupName: '/aws/lambda/' + myFunction.functionName, retention: logs.RetentionDays.ONE_DAY }) loggroup.addSubscriptionFilter("subscription", { filterPattern: logs.FilterPattern.literal("Error"), destination: new logsn.LambdaDestination(errorFunction) }) } }
10 最後に
今回は、色々な場面でのLambdaの設置をAWS CDKで書いてみました。
全ては、全然網羅できていませんが、AWS CDKを使用する場合のパターンが、少し見えてきたように感じています。